package it.polito.connectedcar.tcc.network;

import it.polito.connectedcar.message.OutgoingMessage;
import it.polito.connectedcar.message.TCCFromVehicleMessage;
import it.polito.connectedcar.message.TCCToVehicleMessage;
import it.polito.connectedcar.tcc.TCCTrafficLightPhaseChange;
import it.polito.connectedcar.tcc.TCCVehicleAbstraction;
import it.polito.connectedcar.tcc.tl.TCCTrafficLightsManager;
import it.polito.connectedcar.tcc.zone.TCCZone;
import it.polito.connectedcar.tcc.zone.TCCZonesManager;
import it.polito.connectedcar.tcc.zone.Token;
import it.polito.connectedcar.tcc.zone.TokenLink;
import it.polito.connectedcar.util.Constants;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;

/**
 * This QueueLoop is the main logic of a TCC: it extracts messages from a queue
 * and takes some decisions. Depending on the actionType of the message sent by
 * the vehicle, the TCC will behave differently , possibly discovering where the
 * vehicle is located and sending him th elist of interesting vehicles it has to
 * talk with until next vehicle update
 * 
 * @author Nicola Aresta
 * 
 */
public class TCCVehiclesMessageQueueMainLoop extends Thread {

	private BlockingQueue<TCCFromVehicleMessage> readMessagesQueue;

	private BlockingQueue<OutgoingMessage> toWriteMessagesQueue;

	private TCCZonesManager zonesManager;
	private TCCTrafficLightsManager trafficLightsManager;

	public TCCVehiclesMessageQueueMainLoop(TCCZonesManager zonesManager,
			TCCTrafficLightsManager trafficLightsManager,
			BlockingQueue<TCCFromVehicleMessage> readMessagesQueue,
			BlockingQueue<OutgoingMessage> toWriteMessagesQueue)
			throws Exception {

		this.readMessagesQueue = readMessagesQueue;
		this.toWriteMessagesQueue = toWriteMessagesQueue;
		this.zonesManager = zonesManager;
		this.trafficLightsManager = trafficLightsManager;
	}

	public BlockingQueue<TCCFromVehicleMessage> getReadMessagesQueue() {
		return readMessagesQueue;
	}

	public BlockingQueue<OutgoingMessage> getToWriteMessagesQueue() {
		return toWriteMessagesQueue;
	}

	@Override
	public void run() {
		super.run();
		// stopping criteria ?
		while (true) {
			try {
				
				processMessage(readMessagesQueue.take());

			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

	}

	/**
	 * This is the main logic
	 * 
	 * @param dp
	 */

	public void processMessage(TCCFromVehicleMessage message) {
		// FIXME
		/*
		 * if the message comes from a vehicle , performs the following actions
		 * : 0-check whether it already belongs to a token managed by this TCC
		 * or it is approaching( command code for approaching or use the
		 * belonging token itself) 0-a: if it is approaching, check the token
		 * the vehicle still belongs to and put it into that list of vehicles
		 * (it will be an outside quadrant ) 0-b: if it is already inside this
		 * token( the token specified in the message is managed by this TCC,
		 * proceed with the following actions 1-read the belonging token 2-read
		 * the vehicle's variables 3-check belonging token and belonging
		 * quadrants 4-change the belonging quadrants if necessary 5-send the
		 * belonging quadrants vehicles to it 6-in case it is in the transit
		 * quadrants , get the next-hop token-info from storage and send these
		 * information to it
		 */
		try {
			if (!message.processMessage(null))
				throw new Exception("invalid message ... process failed");

			if (message.isVehicleUpdate()) {
				onVehicleUpdate(message.getTccVehicleAbstraction());
			} else if (message.isIsVehicleGoodBye()) {
				onVehicleGoodbye(message.getTccVehicleAbstraction());
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void onVehicleGoodbye(TCCVehicleAbstraction tva) {

		/*
		 * Check the belonging token : if TCC manages it and the approaching
		 * token is one of the token Links , remove the Vehicle all over the
		 * lists
		 */
		int zoneIndex = zonesManager.getZones().indexOf(tva.getBelongingToken());
		if (zoneIndex == -1) {
			return;
		} else {
			TCCZone zone = zonesManager.getZones().get(zoneIndex);
			synchronized (zone) {
				zone.removeVehicle(tva);
			}
		}
	}

	/**
	 * handle the update message received by TVA ( TCCVehicleAbstraction)
	 * 
	 * @param tva
	 *            : the Vehicle which sent the message
	 */
	private void onVehicleUpdate(TCCVehicleAbstraction tva) {

		// check on ZonesManager if the vehicle already exists
		Token approachingToken = tva.getApproachingToken();
		Token belongingToken = tva.getBelongingToken();

		if (!approachingToken.equals(belongingToken)) {
			// manage when a vehicle is approaching

			int approachingZoneIndex = zonesManager.getZones().indexOf(approachingToken);
			if (approachingZoneIndex == -1) {
				// the approaching zone is unknown... cannot be managed by this
				// TCC
				return;
			}

			TCCZone approachingZone = zonesManager.getZones().get(approachingZoneIndex);

			/* find the quadrant in which this vehicle is */
			int quadrantIndex = approachingZone.getQuadrantIndex(tva.getUtmXPos(), tva.getUtmYPos());
			if (quadrantIndex != -1) {
				/* the TVA is entered the approaching zone */
				synchronized (approachingZone) {
					approachingZone.getQuadrantsVehiclesMap().get(quadrantIndex).add(tva);
					System.out.println("TCC : succefully added "
							+ tva.getVehicleId() + " to "
							+ approachingZone.getT().toString());
				}
				// send a change token message to TVA
				TCCToVehicleMessage m = new TCCToVehicleMessage(Constants.TCC_CHANGE_TOKEN_MESSAGE_CODE);
				m.setChangeToken(approachingZone.getT());
				List<InetSocketAddress> addresses = new ArrayList<InetSocketAddress>();
				addresses.add(tva.getAddress());
				m.setRecipientsAddresses(addresses);
				toWriteMessagesQueue.offer(m);
			} else {
				/*
				 * TVA is outside the approaching Token's area. He needs to talk
				 * with the vehicles inside the TransitVehicles List of the
				 * "TVA belongingToken zone" (those approaching the Zone the TVA
				 * is coming from) and the vehicles inside the belonging
				 * quadrants in the approaching Token zone
				 */
				Set<InetSocketAddress> addresses = new HashSet<InetSocketAddress>();
				synchronized (approachingZone) {
					List<TokenLink> tokenLinks = approachingZone.getTokenLinkList().getTokenLink(tva);
					for (TokenLink tokenLink : tokenLinks) {
						List<TCCVehicleAbstraction> transitVehicles = tokenLink.getTransitVehicles();
						for (TCCVehicleAbstraction v : transitVehicles)
							if (!v.getAddress().equals(tva.getAddress()))
								addresses.add(v.getAddress());

						addresses.addAll(extractVehiclesInInterestingQuadrants(
								tva, approachingZone,
								tokenLink.getBelongingQuadrants()));
					}
				}
				sendUsefulVehiclesMessage(tva, approachingZone, addresses);
			}
		} else {

			/*
			 * the belonging token and the approaching token are the same : it
			 * means the Vehicle is inside the belonging token. We need to know
			 * whether TVA is inside the Token Zone's boundaries or not. If it's
			 * inside the boundaries , check whether the actual belonging
			 * quadrant is a transient one or not , and if yes , find the Token
			 * Links together with their transit vehicles and send them to TVA.
			 * If TVA is not inside the boundaries , search which is the
			 * TokenLinks he is directed to and inform TVA on the Token Link and
			 * the transit vehicles
			 */
			int zoneIndex = zonesManager.getZones().indexOf(belongingToken);
			if (zoneIndex == -1) {
				// the belonging zone is unknown... cannot be managed by this
				// TCC
				return;
			}

			TCCZone zone = zonesManager.getZones().get(zoneIndex);
			int quadrantIndex = zone.getQuadrantIndex(tva.getUtmXPos(),
					tva.getUtmYPos());
			if (quadrantIndex == -1) {
				/*
				 * The token Link is outside the boundaries and perhaps in a
				 * transit area toward some well known Tokens ( tokenLinks of
				 * the actual tokenZone )
				 */
				synchronized (zone) {
					List<TokenLink> links = zone.getTokenLinkList().getTokenLink(tva);
					Set<InetSocketAddress> addresses = new HashSet<InetSocketAddress>();

					/*
					 * extract the transit vehicles from the token Link where
					 * the vehicle position is inside the transit region
					 */
					for (TokenLink tl : links) {
						List<TCCVehicleAbstraction> transitVehicles = tl.getTransitVehicles();
						for (TCCVehicleAbstraction v : transitVehicles)
							if (!v.getAddress().equals(tva.getAddress()))
								addresses.add(v.getAddress());

						addresses.addAll(extractVehiclesInInterestingQuadrants(tva, zone, tl.getBelongingQuadrants()));

						if (!transitVehicles.contains(tva))
							transitVehicles.add(tva);
					}

					sendUsefulVehiclesMessage(tva, zone, addresses);

					if (!links.isEmpty()) {
						TCCToVehicleMessage m = new TCCToVehicleMessage(Constants.TCC_NEW_ZONE_MESSAGE_CODE);
						m.setTokenLinks(links);
						m.setRecipientsAddresses(Arrays.asList(new InetSocketAddress[]{tva.getAddress()}));
						toWriteMessagesQueue.offer(m);
					}
				}

			} else {
				/*
				 * the TVA is inside the Zone boundaries and the 2 possible
				 * cases are : 1) it is in a transit quadrant -> possible
				 * tokenLinks are required to be sent to TVA along with useful
				 * quadrants' vehicles 2) it is in a normal sub-quadrants ->
				 * only useful quadrants ' vehicles have to be sent
				 */
				synchronized (zone) {

					zone.addVehicleToQuadrant(tva, quadrantIndex);

					if (zone.isTransitQuadrant(quadrantIndex)) {
						tva.setCurrentQuadrant(quadrantIndex);
						/* prepare the approaching next token message */
						List<TokenLink> tl = zone.getTokenLinkList().getTokenLink(tva);
						if (!tl.isEmpty()) {
							TCCToVehicleMessage m = new TCCToVehicleMessage(Constants.TCC_NEW_ZONE_MESSAGE_CODE);
							m.setTokenLinks(tl);
							m.setRecipientsAddresses(Arrays
									.asList(new InetSocketAddress[]{tva.getAddress()}));
							toWriteMessagesQueue.offer(m);
						}
					}

					Set<Integer> quadrants = zone
							.getInterestingQuadrants(quadrantIndex);

					Set<InetSocketAddress> addresses = extractVehiclesInInterestingQuadrants(
							tva, zone, quadrants);
					sendUsefulVehiclesMessage(tva, zone, addresses);
				}

				/* traffic lights info */

				sendTrafficLightMessage(tva);
			}
		}
	}

	/**
	 * Checks whether there are useful information about TrafficLights to be
	 * sent to the TVA(TCCVehicleAbstraction). The check is performed by looking
	 * at the current LaneId TVA belongs to.
	 * 
	 * @param tva
	 */
	private void sendTrafficLightMessage(TCCVehicleAbstraction tva) {
		String laneId = tva.getCurrentLaneId();

		TCCTrafficLightPhaseChange nextTrafficLightOnLane = trafficLightsManager.getNextTrafficLightOnLane(laneId);
		if (nextTrafficLightOnLane != null) {
			TCCToVehicleMessage TLmessage = new TCCToVehicleMessage(Constants.TCC_TRAFFIC_LIGHT_UPDATE);
			TLmessage.setTrafficLight(nextTrafficLightOnLane);
			List<InetSocketAddress> recipients = new ArrayList<InetSocketAddress>();
			recipients.add(tva.getAddress());
			TLmessage.setRecipientsAddresses(recipients);
			if (!toWriteMessagesQueue.offer(TLmessage)) {
				System.out.println("the TCC wasn't able to enqueue the Traffic light message to vehicles");
			}
		}
	}

	/**
	 * Prepare and send a message to TVA(TCCVehicleAbstraction) containing the
	 * list of the vehicles TVA has to exchange information with.
	 * 
	 * @param tva
	 *            : the vehicle to which the message has to be sent
	 * @param zone
	 *            : the zone the addresses belongs to.
	 * @param addresses
	 *            : the list of other vehicles TVA will receive
	 */
	private void sendUsefulVehiclesMessage(TCCVehicleAbstraction tva,
			TCCZone zone, Set<InetSocketAddress> addresses) {

		TCCToVehicleMessage message = new TCCToVehicleMessage(Constants.TCC_VEHICLES_LIST_MESSAGE_CODE);

		Map<Token, Set<InetSocketAddress>> vehiclesMap = new HashMap<Token, Set<InetSocketAddress>>();
		vehiclesMap.put(zone.getT(), addresses);
		message.setVehicles(vehiclesMap);

		/* set the message recipients */
		List<InetSocketAddress> recipients = new ArrayList<InetSocketAddress>();
		recipients.add(tva.getAddress());
		message.setRecipientsAddresses(recipients);

		toWriteMessagesQueue.offer(message);
	}

	/**
	 * Extract all the vehicles inside the TCCZone belonging to the quadrants
	 * passed as input
	 * 
	 * @param tva
	 * @param zone
	 * @param quadrants
	 * @return the list of all the vehicles' addresses containing in the
	 *         quadrants belonging to the zone
	 */
	private Set<InetSocketAddress> extractVehiclesInInterestingQuadrants(
			TCCVehicleAbstraction tva, TCCZone zone,
			Collection<Integer> quadrants) {

		synchronized (zone) {
			Set<InetSocketAddress> addresses = new HashSet<InetSocketAddress>();
			for (Integer quadrant : quadrants) {
				Set<TCCVehicleAbstraction> veh = zone.getQuadrantsVehiclesMap().get(quadrant);
				for (TCCVehicleAbstraction va : veh) {
					if (!tva.getAddress().equals(va.getAddress()))
						addresses.add(va.getAddress());
				}
			}
			return addresses;
		}
	}
}
